feat(swarm-protocol): open mirror of the Arcanea swarm packaging protocol#78
feat(swarm-protocol): open mirror of the Arcanea swarm packaging protocol#78frankxai wants to merge 6 commits into
Conversation
…ocol Public, forkable mirror of @arcanea/swarm-protocol (per SIP: the orchestration pattern is forkable; encoded-self is not). The chain-agnostic Swarm Package Manifest that backs the Web3 agent-swarm marketplace — topology + agent specs + license terms + bps royalty split + SIP attestation + per-chain bindings, with a dependency-free validator, deterministic canonicalizer, pack-swarm CLI, and the three reference manifests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017NdpPwQn5L3o77D1f9PMp9
|
Deployment failed with the following error: |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces the @arcanea/swarm-protocol package, which provides a chain-agnostic packaging layer, validator, CLI, and reference manifests for Arcanea agent swarms. The code review highlights several critical improvements for robustness and type safety. Specifically, defensive checks should be added to the validator in schema.ts to prevent runtime TypeErrors when handling null or non-object elements in agents, royalty.recipients, and chains. Additionally, the validateAgent type guard should be corrected to return false upon validation failures, property spreading in buildManifest should be reordered to protect default fields from being overwritten, and the FNV-1a fingerprinting function should be updated to hash UTF-8 bytes instead of UTF-16 code units.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (!isNonEmptyString(topo.queen)) { | ||
| errors.push('topology.queen: required agent id'); | ||
| } else if (agentIds.size && !agentIds.has(topo.queen)) { | ||
| errors.push(`topology.queen: '${topo.queen}' is not in agents`); | ||
| } else { | ||
| const q = (agents as AgentPackage[]).find((a) => a.id === topo.queen); | ||
| if (q && q.role !== 'queen') errors.push(`topology.queen: agent '${topo.queen}' must have role 'queen'`); | ||
| } |
There was a problem hiding this comment.
If agents is undefined or not an array, calling (agents as AgentPackage[]).find will throw a TypeError at runtime. Additionally, if agents contains null or non-object elements, accessing a.id will throw. Since this validator is designed to be dependency-free and return a structured validation result instead of throwing, we should add defensive checks to ensure agents is an array and that each element is a valid object before accessing properties.
| if (!isNonEmptyString(topo.queen)) { | |
| errors.push('topology.queen: required agent id'); | |
| } else if (agentIds.size && !agentIds.has(topo.queen)) { | |
| errors.push(`topology.queen: '${topo.queen}' is not in agents`); | |
| } else { | |
| const q = (agents as AgentPackage[]).find((a) => a.id === topo.queen); | |
| if (q && q.role !== 'queen') errors.push(`topology.queen: agent '${topo.queen}' must have role 'queen'`); | |
| } | |
| if (!isNonEmptyString(topo.queen)) { | |
| errors.push('topology.queen: required agent id'); | |
| } else if (agentIds.size && !agentIds.has(topo.queen)) { | |
| errors.push(`topology.queen: '${topo.queen}' is not in agents`); | |
| } else if (Array.isArray(agents)) { | |
| const q = (agents as any[]).find((a) => a && typeof a === 'object' && a.id === topo.queen); | |
| if (q && q.role !== 'queen') errors.push(`topology.queen: agent '${topo.queen}' must have role 'queen'`); | |
| } |
| let sum = 0; | ||
| recips.forEach((r, i) => { | ||
| const ro = r as Record<string, unknown>; | ||
| if (!isNonEmptyString(ro.label)) errors.push(`royalty.recipients[${i}].label: required`); | ||
| if (!isNonEmptyString(ro.address)) errors.push(`royalty.recipients[${i}].address: required (use '<placeholder>')`); | ||
| if (!isInt(ro.bps) || (ro.bps as number) < 0) errors.push(`royalty.recipients[${i}].bps: required non-negative int`); | ||
| else sum += ro.bps as number; | ||
| }); |
There was a problem hiding this comment.
If royalty.recipients contains null or non-object elements, accessing ro.label or other properties will throw a TypeError at runtime. We should add a defensive check to ensure each recipient is a valid object before validating its properties.
let sum = 0;
recips.forEach((r, i) => {
if (typeof r !== 'object' || r === null) {
errors.push(`royalty.recipients[${i}]: must be an object`);
return;
}
const ro = r as Record<string, unknown>;
if (!isNonEmptyString(ro.label)) errors.push(`royalty.recipients[${i}].label: required`);
if (!isNonEmptyString(ro.address)) errors.push(`royalty.recipients[${i}].address: required (use '<placeholder>')`);
if (!isInt(ro.bps) || (ro.bps as number) < 0) errors.push(`royalty.recipients[${i}].bps: required non-negative int`);
else sum += ro.bps as number;
});| } else { | ||
| chains.forEach((c, i) => { | ||
| const co = c as Record<string, unknown>; | ||
| if (!CHAINS.includes(co.chain as Chain)) errors.push(`chains[${i}].chain: base | polygon | solana`); | ||
| if (!STANDARDS.includes(co.standard as string)) errors.push(`chains[${i}].standard: invalid token standard`); | ||
| if (!isNonEmptyString(co.network)) errors.push(`chains[${i}].network: required label`); | ||
| }); |
There was a problem hiding this comment.
If chains contains null or non-object elements, accessing co.chain or other properties will throw a TypeError at runtime. We should add a defensive check to ensure each chain binding is a valid object before validating its properties.
} else {
chains.forEach((c, i) => {
if (typeof c !== 'object' || c === null) {
errors.push(`chains[${i}]: must be an object`);
return;
}
const co = c as Record<string, unknown>;
if (!CHAINS.includes(co.chain as Chain)) errors.push(`chains[${i}].chain: base | polygon | solana`);
if (!STANDARDS.includes(co.standard as string)) errors.push(`chains[${i}].standard: invalid token standard`);
if (!isNonEmptyString(co.network)) errors.push(`chains[${i}].network: required label`);
});| function validateAgent(a: unknown, i: number, errors: string[]): a is AgentPackage { | ||
| const p = `agents[${i}]`; | ||
| if (typeof a !== 'object' || a === null) { | ||
| errors.push(`${p}: must be an object`); | ||
| return false; | ||
| } | ||
| const o = a as Record<string, unknown>; | ||
| if (!isNonEmptyString(o.id)) errors.push(`${p}.id: required non-empty string`); | ||
| if (!isNonEmptyString(o.name)) errors.push(`${p}.name: required non-empty string`); | ||
| if (!isNonEmptyString(o.title)) errors.push(`${p}.title: required non-empty string`); | ||
| if (o.role !== 'queen' && o.role !== 'worker') errors.push(`${p}.role: must be 'queen' | 'worker'`); | ||
| if (!isNonEmptyString(o.domain)) errors.push(`${p}.domain: required non-empty string`); | ||
| if (!isNonEmptyString(o.voice)) errors.push(`${p}.voice: required non-empty string`); | ||
| if (!ELEMENTS.includes(o.element as ElementAffinity)) errors.push(`${p}.element: invalid element`); | ||
| if (!isStringArray(o.capabilities)) errors.push(`${p}.capabilities: required string[]`); | ||
| if (o.systemPrompt !== undefined && !isString(o.systemPrompt)) errors.push(`${p}.systemPrompt: must be string`); | ||
| if (o.specUri !== undefined && !isString(o.specUri)) errors.push(`${p}.specUri: must be string`); | ||
| if (o.systemPrompt === undefined && o.specUri === undefined) { | ||
| errors.push(`${p}: must carry either systemPrompt or specUri (the licensed IP must be locatable)`); | ||
| } | ||
| return true; | ||
| } |
There was a problem hiding this comment.
The validateAgent function is declared as a TypeScript type guard (a is AgentPackage), but it returns true even when there are validation errors (as long as a is an object). This violates the contract of a type guard and can lead to type-safety issues downstream. We should track if any errors were added during the validation of this agent and return false if so.
function validateAgent(a: unknown, i: number, errors: string[]): a is AgentPackage {
const p = `agents[${i}]`;
if (typeof a !== 'object' || a === null) {
errors.push(`${p}: must be an object`);
return false;
}
const o = a as Record<string, unknown>;
const initialErrorsCount = errors.length;
if (!isNonEmptyString(o.id)) errors.push(`${p}.id: required non-empty string`);
if (!isNonEmptyString(o.name)) errors.push(`${p}.name: required non-empty string`);
if (!isNonEmptyString(o.title)) errors.push(`${p}.title: required non-empty string`);
if (o.role !== 'queen' && o.role !== 'worker') errors.push(`${p}.role: must be 'queen' | 'worker'`);
if (!isNonEmptyString(o.domain)) errors.push(`${p}.domain: required non-empty string`);
if (!isNonEmptyString(o.voice)) errors.push(`${p}.voice: required non-empty string`);
if (!ELEMENTS.includes(o.element as ElementAffinity)) errors.push(`${p}.element: invalid element`);
if (!isStringArray(o.capabilities)) errors.push(`${p}.capabilities: required string[]`);
if (o.systemPrompt !== undefined && !isString(o.systemPrompt)) errors.push(`${p}.systemPrompt: must be string`);
if (o.specUri !== undefined && !isString(o.specUri)) errors.push(`${p}.specUri: must be string`);
if (o.systemPrompt === undefined && o.specUri === undefined) {
errors.push(`${p}: must carry either systemPrompt or specUri (the licensed IP must be locatable)`);
}
return errors.length === initialErrorsCount;
}| export function buildManifest(input: ManifestInput, now: string = new Date().toISOString()): SwarmManifest { | ||
| const manifest: SwarmManifest = { | ||
| manifestVersion: SWARM_MANIFEST_VERSION, | ||
| attestation: input.attestation ?? arcaneaAttestation(), | ||
| createdAt: input.createdAt ?? now, | ||
| updatedAt: input.updatedAt ?? now, | ||
| ...stripUndefined(input), | ||
| } as SwarmManifest; | ||
| return assertValidSwarmManifest(manifest); | ||
| } |
There was a problem hiding this comment.
Spreading stripUndefined(input) last allows any extra properties on input (such as manifestVersion if passed from a raw JS object at runtime) to overwrite the defaults. To ensure runtime safety and robustness, we should spread stripUndefined(input) first, and then apply the default/required fields.
| export function buildManifest(input: ManifestInput, now: string = new Date().toISOString()): SwarmManifest { | |
| const manifest: SwarmManifest = { | |
| manifestVersion: SWARM_MANIFEST_VERSION, | |
| attestation: input.attestation ?? arcaneaAttestation(), | |
| createdAt: input.createdAt ?? now, | |
| updatedAt: input.updatedAt ?? now, | |
| ...stripUndefined(input), | |
| } as SwarmManifest; | |
| return assertValidSwarmManifest(manifest); | |
| } | |
| export function buildManifest(input: ManifestInput, now: string = new Date().toISOString()): SwarmManifest { | |
| const manifest: SwarmManifest = { | |
| ...stripUndefined(input), | |
| manifestVersion: SWARM_MANIFEST_VERSION, | |
| attestation: input.attestation ?? arcaneaAttestation(), | |
| createdAt: input.createdAt ?? now, | |
| updatedAt: input.updatedAt ?? now, | |
| } as SwarmManifest; | |
| return assertValidSwarmManifest(manifest); | |
| } |
| export function manifestFingerprint(manifest: SwarmManifest): string { | ||
| const text = canonicalize(manifest); | ||
| let hash = 0x811c9dc5; | ||
| for (let i = 0; i < text.length; i++) { | ||
| hash ^= text.charCodeAt(i); | ||
| hash = Math.imul(hash, 0x01000193); | ||
| } | ||
| return (hash >>> 0).toString(16).padStart(8, '0'); | ||
| } |
There was a problem hiding this comment.
Standard FNV-1a operates on bytes. Hashing UTF-16 code units directly via charCodeAt is non-standard and can lead to unexpected behavior with multi-byte characters (like emojis or non-ASCII text) because it XORs a 16-bit value into a 32-bit hash. Using TextEncoder to get UTF-8 bytes is standard and robust.
| export function manifestFingerprint(manifest: SwarmManifest): string { | |
| const text = canonicalize(manifest); | |
| let hash = 0x811c9dc5; | |
| for (let i = 0; i < text.length; i++) { | |
| hash ^= text.charCodeAt(i); | |
| hash = Math.imul(hash, 0x01000193); | |
| } | |
| return (hash >>> 0).toString(16).padStart(8, '0'); | |
| } | |
| export function manifestFingerprint(manifest: SwarmManifest): string { | |
| const text = canonicalize(manifest); | |
| const bytes = new TextEncoder().encode(text); | |
| let hash = 0x811c9dc5; | |
| for (let i = 0; i < bytes.length; i++) { | |
| hash ^= bytes[i]; | |
| hash = Math.imul(hash, 0x01000193); | |
| } | |
| return (hash >>> 0).toString(16).padStart(8, '0'); | |
| } |
Code Review —
|
Mirror of the arcanea-ai-app hardening pass. CI root cause was a new package.json without a matching pnpm-lock.yaml update; regenerated the lockfile. Validator: null/object guards (never throws), real validateAgent pass/fail, specUri scheme allow-list, new checkDeployReady() strict gate. pack.ts: safer buildManifest spread + UTF-8 FNV-1a fingerprint. cli.ts: exported, unit-testable main(). +9 tests (23/23). manifest: guardian-heart Wind -> Water. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017NdpPwQn5L3o77D1f9PMp9
Code Review —
|
| Category | Finding | Severity |
|---|---|---|
| Bug | Worker role not validated in validateSwarmManifest |
Medium |
| Security | Unsanitized path in CLI resolvePath |
Low |
| Design | export * star exports in index.ts |
Low |
| Design | PLACEHOLDER unexported |
Low |
| Config | ignoreDeprecations: "6.0" not tracked |
Low |
| Tests | Missing negative test for worker-role enforcement | Low |
The worker role check and the PLACEHOLDER export are worth fixing before merge. The rest are advisory.
Test Packages CI failed at `actions/setup-node@v5` with "Unable to locate
executable file: pnpm": setup-node's default package-manager-cache detects
pnpm (root packageManager field) and runs `pnpm store path` before pnpm is
installed. Move `pnpm/action-setup` ahead of `setup-node` (and set cache: pnpm
explicitly) so the cache step finds pnpm.
Also remove an orphaned gitlink at a literal Windows path
('C:\Users\Frank\.claude\plugins\marketplaces\claude-plugins-official') that
had no .gitmodules entry and made `git submodule foreach` exit 128 during
checkout cleanup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NdpPwQn5L3o77D1f9PMp9
Mirror of arcanea-ai-app. validateSwarmManifest now enforces that every topology.workers entry has role 'worker' (previously only the queen's role was checked). Export PLACEHOLDER so consumers share one deploy-gate sentinel. +1 negative test (24/24). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017NdpPwQn5L3o77D1f9PMp9
Code Review —
|
Mirror of arcanea-ai-app. gateAlignment (optional) is now checked against the 10 canonical Guardian Gates instead of passing any string. +2 tests (gateAlignment allow-list, secondaryRoyaltyBps over-cap). 26/26. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017NdpPwQn5L3o77D1f9PMp9
Code Review —
|
| Area | Status |
|---|---|
| Code quality | ✅ Clean |
| TypeScript types | ✅ Well-typed |
| Zero-dep validator | ✅ Good design |
| Test coverage | |
| Security | |
| Bugs | now |
private flag |
Recommend addressing items 2, 3, and 6 before merge; items 1, 4, and the test gaps are lower priority but worth tracking.
Mirror of arcanea-ai-app. The deploy gate now flags agent specUris still containing a placeholder marker, consistent with its address/metadataUri checks — nothing with placeholders can be marked deploy-ready. +2 tests (deploy-ready happy path, specUri placeholder rejection). 28/28. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017NdpPwQn5L3o77D1f9PMp9
Code Review —
|
| Category | Status |
|---|---|
| Architecture | ✅ Clean, well-separated |
| TypeScript types | ✅ Strict, accurate |
| Validation | ✅ Accumulates all errors, two-tier (structural + deploy) |
| Test coverage | |
| Security | ✅ No critical issues; placeholder gating is correct |
| Performance | ✅ Acceptable for expected scale |
| CI fix | ✅ Correct |
| Lock file cleanup | |
| TS 6 dependency |
Great foundation — address the TS 6 concern and the missing CLI tests and this is ready.
🤖 Generated with Claude Code
What this is
Public, forkable mirror of
@arcanea/swarm-protocol— the chain-agnostic packaging layer behind the Web3 agent-swarm marketplace (built in the production appfrankxai/arcanea-ai-app#175). Per SIP: the orchestration pattern is forkable; encoded-self is not.A Swarm Package Manifest is the unit that gets tokenized as a Swarm License NFT and metered per-invocation across Base, Polygon, and Solana. It composes: topology (queen + worker mesh) + portable agent specs + license terms + bps royalty split (sums to 10000) + SIP attestation + per-chain bindings.
Contents
packages/swarm-protocol/— types, dependency-free validator, deterministic canonicalizer,pack-swarmCLI.creative-author-council,catalog-kits,guardian-orchestration.Zero runtime dependencies — compiles with plain
tsc, tests withnode --test(14/14).Verify
🤖 Generated with Claude Code
Generated by Claude Code